package org.xx.armory.spring5.jdbc;

import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.core.SqlParameterValue;
import org.springframework.jdbc.core.SqlReturnResultSet;

import java.sql.Types;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static java.sql.Types.NVARCHAR;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.xx.armory.commons.Validators.rejectIfBlank;
import static org.xx.armory.commons.Validators.rejectIfNull;

/**
 * 用于构造SQL和参数的类。
 */
public final class SqlAndArgsBuilder {
    public static final String RETURN_RESULT_SET_ARG_NAME = "result_set";

    private final SqlStringBuilder sqlBuilder;
    private final List<SqlParameter> args;

    /**
     * 初始化 {@link SqlAndArgsBuilder}。
     */
    public SqlAndArgsBuilder() {
        this.sqlBuilder = new SqlStringBuilder("", "\n", "");
        this.args = new ArrayList<>();
    }

    /**
     * 添加一段SQL。
     *
     * @param sql
     *         待添加的SQL。
     * @return 此构造类本身。
     */
    public SqlAndArgsBuilder prepend(
            String sql
    ) {
        this.sqlBuilder.prepend(sql);

        return this;
    }

    /**
     * 添加一段SQL。
     *
     * @param sql
     *         待添加的SQL。
     * @return 此构造类本身。
     */
    public SqlAndArgsBuilder append(
            String sql
    ) {
        this.sqlBuilder.append(sql);
        return this;
    }

    /**
     * 创建一个动态SQL构造类。
     *
     * <p>如果最后生成的SQL不是空字符串，那么自动附加前缀和后缀。</p>
     *
     * @param prefix
     *         动态SQL的前缀。
     * @param joint
     *         动态SQL的连接字符串。
     * @param suffix
     *         动态SQL的后缀。
     * @return 动态SQL构造类。
     */
    public final DynamicSql dynamic(
            String prefix,
            String joint,
            String suffix
    ) {
        return new DynamicSql(prefix, joint, suffix);
    }

    /**
     * 添加一个定位参数。
     * <p>定位参数不需要指定参数名，用于调用存储过程。</p>
     *
     * @param arg
     *         参数值。
     * @param argType
     *         参数类型，来自于 {@link java.sql.Types}。
     * @return 此构造类本身。
     * @throws IllegalArgumentException
     *         如果参数 {@code name} 是{@code null} 或者只包含空白字符。
     */
    public SqlAndArgsBuilder addPositional(
            Object arg,
            int argType
    ) {
        return addArg("_" + this.args.size(), arg, argType, 0);
    }

    /**
     * 添加一个定位参数。
     * <p>定位参数不需要指定参数名，用于调用存储过程。</p>
     *
     * @param arg
     *         参数值。
     * @param argType
     *         参数类型，来自于 {@link java.sql.Types}。
     * @param size
     *         参数最大长度，{@code 0} 表示忽略。
     * @return 此构造类本身。
     * @throws IllegalArgumentException
     *         如果参数 {@code name} 是{@code null} 或者只包含空白字符。
     */
    public SqlAndArgsBuilder addPositional(
            Object arg,
            int argType,
            int size
    ) {
        return addArg("_" + this.args.size(), arg, argType, size);
    }

    /**
     * 添加一个字符串参数。
     *
     * @param name
     *         参数名。自动去掉首尾空格。
     * @param arg
     *         参数值。
     * @return 此构造类本身。
     * @throws IllegalArgumentException
     *         如果参数 {@code name} 是{@code null} 或者只包含空白字符。
     */
    public SqlAndArgsBuilder addArg(
            String name,
            CharSequence arg
    ) {
        return addArg(name, arg, NVARCHAR, 0);
    }

    /**
     * 添加一个参数。
     *
     * @param name
     *         参数名。自动去掉首尾空格。
     * @param arg
     *         参数值。
     * @param argType
     *         参数类型，来自于 {@link java.sql.Types}。
     * @return 此构造类本身。
     * @throws IllegalArgumentException
     *         如果参数 {@code name} 是{@code null} 或者只包含空白字符。
     */
    public SqlAndArgsBuilder addArg(
            String name,
            Object arg,
            int argType
    ) {
        return addArg(name, arg, argType, 0);
    }

    /**
     * 添加一个传出参数。
     *
     * @param name
     *         参数名。自动去掉首尾空格。
     * @param argType
     *         参数类型，来自于 {@link java.sql.Types}。
     * @return 此构造类本身。
     * @throws IllegalArgumentException
     *         如果参数 {@code name} 是{@code null} 或者只包含空白字符。
     */
    public SqlAndArgsBuilder addOut(
            String name,
            int argType
    ) {
        name = rejectIfBlank(name, "name").trim();

        return addArg(new SqlOutParameter(name, argType));
    }

    /**
     * 添加一个传出参数，此传出参数表示一个结果集。
     *
     * @return 此构造类本身。
     */
    public SqlAndArgsBuilder addOutResult() {
        return addArg(new SqlOutParameter(RETURN_RESULT_SET_ARG_NAME, Types.REF_CURSOR));
    }

    /**
     * 添加一个返回参数，此返回参数表示一个结果集。
     *
     * @return 此构造类本身。
     */
    public SqlAndArgsBuilder addReturnResult() {
        return addArg(new SqlReturnResultSet(RETURN_RESULT_SET_ARG_NAME, (rs) -> null));
    }

    /**
     * 添加一个参数。
     *
     * @param arg
     *         参数。
     * @return 此构造类本身。
     */
    public SqlAndArgsBuilder addArg(
            SqlParameter arg
    ) {
        this.args.add(arg);
        return this;
    }

    /**
     * 添加一个参数。
     *
     * @param name
     *         参数名。自动去掉首尾空格。
     * @param arg
     *         参数值。
     * @param argType
     *         参数类型，来自于 {@link java.sql.Types}。
     * @param size
     *         参数最大长度，{@code 0} 表示忽略。
     * @return 此构造类本身。
     * @throws IllegalArgumentException
     *         如果参数 {@code name} 是{@code null} 或者只包含空白字符。
     */
    public SqlAndArgsBuilder addArg(
            String name,
            Object arg,
            int argType,
            int size
    ) {
        name = rejectIfBlank(name, "name").trim();

        // 针对size参数对字符串进行预处理。
        if (arg instanceof CharSequence) {
            if (size > 0 && ((CharSequence) arg).length() > size) {
                arg = ((CharSequence) arg).subSequence(0, size);
            }
            arg = arg.toString();
        }

        this.args.add(new SqlParameterValue(new SqlParameter(name, argType), arg));
        return this;
    }

    /**
     * 构造结果SQL。
     *
     * @return 结果SQL。
     */
    public String buildSql() {
        return this.sqlBuilder.build();
    }

    /**
     * 获取已添加的参数集合。
     *
     * @return 所有已添加的参数集合。
     */
    public Map<String, SqlParameter> getArgs() {
        final var result = new LinkedHashMap<String, SqlParameter>(this.args.size(), 1.0F, false);

        for (final var arg : this.args) {
            result.put(arg.getName(), arg);
        }

        return result;
    }

    private static abstract class SqlChunkContainer {
        private final SqlStringBuilder sql;

        protected SqlChunkContainer(
                SqlStringBuilder sql
        ) {
            this.sql = rejectIfNull(sql, "sql");
        }

        /**
         * 添加一段SQL。
         *
         * <p>如果待添加的SQL是空字符串，那么不添加SQL。/p>
         *
         * @param sql
         *         待添加的SQL。
         */
        void append(
                String sql
        ) {
            if (!sql.isEmpty()) {
                this.sql.append(sql);
            }
        }

        /**
         * 如果指定的数据<strong>不是</strong> {@code null}，也<strong>不是</strong> 空字符串，也<strong>不是</strong> 空集合则添加动态SQL。
         *
         * @param value
         *         待检查的数据。
         * @param sql
         *         待添加的动态SQL。
         * @return 是否添加了动态SQL。
         */
        protected final boolean addIfNotNullInternal(
                Object value,
                String sql
        ) {
            rejectIfNull(sql, "sql");

            if (value != null
                    && !(value instanceof CharSequence && ((CharSequence) value).length() == 0)
                    && !(value instanceof Collection && ((Collection<?>) value).size() == 0)
            ) {
                this.append(sql);
                return true;
            }

            return false;
        }

        /**
         * 如果指定的数据<strong>不是</strong> {@code null}，并且包含空白字符之外的其它字符，则添加动态SQL。
         *
         * @param value
         *         待检查的数据。
         * @param sql
         *         待添加的动态SQL。
         * @return 是否添加了动态SQL。
         */
        protected final boolean addIfNotBlankInternal(
                CharSequence value,
                String sql
        ) {
            rejectIfNull(sql, "sql");

            if (!isBlank(value)) {
                this.append(sql);
                return true;
            }

            return false;
        }

        /**
         * 如果指定的数据<strong>是</strong> {@code null}，或者<strong>是</strong> 空字符串，或者<strong>是</strong> 空集合则添加动态SQL。
         *
         * @param value
         *         待检查的数据。
         * @param sql
         *         待添加的动态SQL。
         * @return 是否添加了动态SQL。
         */
        protected final boolean addIfNullInternal(
                Object value,
                String sql
        ) {
            rejectIfNull(sql, "sql");

            if (value == null
                    || value instanceof CharSequence && ((CharSequence) value).length() == 0
                    || value instanceof Collection && ((Collection<?>) value).size() == 0
            ) {
                this.append(sql);
                return true;
            }

            return false;
        }

        /**
         * 如果指定的数据<strong>是</strong> {@code null}，或者只包含空白字符，则添加动态SQL。
         *
         * @param value
         *         待检查的数据。
         * @param sql
         *         待添加的动态SQL。
         * @return 是否添加了动态SQL。
         */
        protected final boolean addIfBlankInternal(
                CharSequence value,
                String sql
        ) {
            rejectIfNull(sql, "sql");

            if (isBlank(value)) {
                this.append(sql);
                return true;
            }

            return false;
        }

        protected final <T> boolean addIfEqualsInternal(
                T value,
                T expected,
                String sql
        ) {
            rejectIfNull(sql, "sql");

            if (value != null && Objects.equals(value, expected)) {
                this.append(sql);
                return true;
            }

            return false;
        }

        public final <T> boolean addIfNotEqualsInternal(
                T value,
                T expected,
                String sql
        ) {
            rejectIfNull(sql, "sql");

            if (value != null && !Objects.equals(value, expected)) {
                this.append(sql);
                return true;
            }

            return false;
        }

        public final <T> boolean addIfInInternal(
                T value,
                Collection<T> c,
                String sql
        ) {
            rejectIfNull(sql, "sql");
            rejectIfNull(c, "c");

            if (value != null && c.contains(value)) {
                this.append(sql);
                return true;
            }

            return false;
        }

        public final <T> boolean addIfNotInInternal(
                T value,
                Collection<T> c,
                String sql
        ) {
            rejectIfNull(sql, "sql");
            rejectIfNull(c, "c");

            if (value != null && !c.contains(value)) {
                this.append(sql);
                return true;
            }

            return false;
        }

        protected final String buildSql() {
            return this.sql.build();
        }

        protected final void clear() {
            this.sql.clear();
        }
    }

    /**
     * 动态SQL构造类。
     *
     * <p>如果最后生成的SQL不是空字符串，那么自动附加前缀和后缀。</p>
     */
    public final class DynamicSql
            extends SqlChunkContainer {
        /**
         * 初始化动态SQL构造类。
         *
         * @param prefix
         *         动态SQL的前缀，如果是{@code null}则使用空字符串。
         * @param joint
         *         动态SQL的连接字符串，如果是{@code null}则使用单一空格。
         * @param suffix
         *         动态SQL的后缀，如果是{@code null}则使用空字符串。
         */
        private DynamicSql(
                String prefix,
                String joint,
                String suffix
        ) {
            super(
                    new SqlStringBuilder(
                            prefix != null ? prefix : "",
                            joint != null ? joint : " ",
                            suffix != null ? suffix : ""
                    )
            );
        }

        /**
         * 根据动态内容构造动态SQL，并将构造完成的SQL和对应的参数、参数类型加入到 {@link SqlAndArgsBuilder} 构造类。
         * <p>调用此方法后，此动态构造类的动态SQL、参数和参数类型都被清空。</p>
         *
         * @return 上级 {@link SqlAndArgsBuilder} 构造类。
         */
        public final SqlAndArgsBuilder end() {
            SqlAndArgsBuilder.this.append(buildSql());

            clear();

            return SqlAndArgsBuilder.this;
        }

        /**
         * 添加动态SQL。
         *
         * @param value
         *         待检查的数据。
         * @param sql
         *         待添加的sql。
         * @return 此动态SQL构造类本身。
         * @see SqlChunkContainer#addIfNotNullInternal(Object, String)
         */
        public final DynamicSql addIfNotNull(
                Object value,
                String sql
        ) {
            addIfNotNullInternal(value, sql);

            return this;
        }

        /**
         * 添加动态SQL。
         *
         * @param value
         *         待检查的数据。
         * @param sql
         *         待添加的sql。
         * @return 此动态SQL构造类本身。
         * @see SqlChunkContainer#addIfNotBlankInternal(CharSequence, String)
         */
        public final DynamicSql addIfNotBlank(
                CharSequence value,
                String sql
        ) {
            addIfNotBlankInternal(value, sql);

            return this;
        }

        /**
         * 如果指定的数据<strong>是</strong> {@code null}，或者<strong>是</strong> 空字符串，或者<strong>是</strong> 空集合则添加动态SQL。
         *
         * @param value
         *         待检查的数据。
         * @param sql
         *         待添加的动态SQL。
         * @return 此动态SQL构造类本身。
         */
        public final DynamicSql addIfNull(
                Object value,
                String sql
        ) {
            addIfNullInternal(value, sql);

            return this;
        }

        /**
         * 如果指定的数据<strong>是</strong> {@code null}，或者只包含空白字符，则添加动态SQL。
         *
         * @param value
         *         待检查的数据。
         * @param sql
         *         待添加的动态SQL。
         * @return 此动态SQL构造类本身。
         */
        public final DynamicSql addIfBlank(
                CharSequence value,
                String sql
        ) {
            addIfBlankInternal(value, sql);

            return this;
        }

        public final <T> DynamicSql addIfEquals(
                T value,
                T expected,
                String sql
        ) {
            addIfEqualsInternal(value, expected, sql);

            return this;
        }

        public final <T> DynamicSql addIfNotEquals(
                T value,
                T expected,
                String sql
        ) {
            addIfNotEqualsInternal(value, expected, sql);

            return this;
        }

        public final <T> DynamicSql addIfIn(
                T value,
                Collection<T> c,
                String sql
        ) {
            addIfInInternal(value, c, sql);

            return this;
        }

        public final <T> DynamicSql addIfNotIn(
                T value,
                Collection<T> c,
                String sql
        ) {
            addIfNotInInternal(value, c, sql);

            return this;
        }

        public final CaseSql addCase() {
            return new CaseSql();
        }

        public final class CaseSql {
            private boolean found;

            private CaseSql() {
                this.found = false;
            }

            /**
             * 添加动态SQL。
             *
             * @param value
             *         待检查的数据。
             * @param sql
             *         待添加的动态SQL。
             * @return 此动态SQL构造类本身。
             * @see SqlChunkContainer#addIfNotNullInternal(Object, String)
             */
            public final CaseSql addIfNotNull(
                    Object value,
                    String sql
            ) {
                if (!this.found) {
                    this.found = addIfNotNullInternal(value, sql);
                }

                return this;
            }

            /**
             * 添加动态SQL。
             *
             * @param value
             *         待检查的数据。
             * @param sql
             *         待添加的动态SQL。
             * @return 此动态SQL构造类本身。
             * @see SqlChunkContainer#addIfNotBlankInternal(CharSequence, String)
             */
            public final CaseSql addIfNotBlank(
                    CharSequence value,
                    String sql
            ) {
                if (!this.found) {
                    this.found = addIfNotBlankInternal(value, sql);
                }

                return this;
            }

            /**
             * 如果指定的数据<strong>是</strong> {@code null}，或者<strong>是</strong> 空字符串，或者<strong>是</strong> 空集合则添加动态SQL。
             *
             * @param value
             *         待检查的数据。
             * @param sql
             *         待添加的动态SQL。
             * @return 此动态SQL构造类本身。
             */
            public final CaseSql addIfNull(
                    Object value,
                    String sql
            ) {
                if (!this.found) {
                    this.found = addIfNullInternal(value, sql);
                }

                return this;
            }

            /**
             * 如果指定的数据<strong>是</strong> {@code null}，或者只包含空白字符，则添加动态SQL。
             *
             * @param value
             *         待检查的数据。
             * @param sql
             *         待添加的动态SQL。
             * @return 此动态SQL构造类本身。
             */
            public final CaseSql addIfBlank(
                    CharSequence value,
                    String sql
            ) {
                if (!this.found) {
                    this.found = addIfBlankInternal(value, sql);
                }

                return this;
            }

            public final <T> CaseSql addIfEquals(
                    T value,
                    T expected,
                    String sql
            ) {
                if (!this.found) {
                    this.found = addIfEqualsInternal(value, expected, sql);
                }

                return this;
            }

            public final <T> CaseSql addIfNotEquals(
                    T value,
                    T expected,
                    String sql
            ) {
                if (!this.found) {
                    this.found = addIfNotEqualsInternal(value, expected, sql);
                }

                return this;
            }

            public final <T> CaseSql addIfIn(
                    T value,
                    Collection<T> c,
                    String sql
            ) {
                if (!this.found) {
                    this.found = addIfInInternal(value, c, sql);
                }

                return this;
            }

            public final <T> CaseSql addIfNotIn(
                    T value,
                    Collection<T> c,
                    String sql
            ) {
                if (!this.found) {
                    this.found = addIfNotInInternal(value, c, sql);
                }

                return this;
            }

            public DynamicSql end() {
                return DynamicSql.this;
            }
        }
    }
}
